--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 8133c45545c4fb35ed0355840f9da17de4885896
Parents : 0ea06ae
Author : Mark Qvist <bc7291552be7a58f361522990465165c>
Signature : T66BB85Valid, signed by author
Date : 2026-07-19T12:46:54+02:00
Map optimizations
Changes
12 files changed, 160 insertions(+), 354 deletions(-)
Diff
diff --git a/sbapp/mapview/__init__.py b/sbapp/mapview/__init__.py
index 3fc9a979..cfad7f0f 100644
--- a/sbapp/mapview/__init__.py
+++ b/sbapp/mapview/__init__.py
@@ -1,20 +1,6 @@
-# coding=utf-8
-"""
-MapView
-=======
-
-MapView is a Kivy widget that display maps.
-"""
from mapview.source import MapSource
from mapview.types import Bbox, Coordinate
-from mapview.view import (
- MapLayer,
- MapMarker,
- CustomMapMarker,
- MapMarkerPopup,
- MapView,
- MarkerMapLayer,
-)
+from mapview.view import MapLayer, MapMarker, CustomMapMarker, MapMarkerPopup, MapView, MarkerMapLayer,
__all__ = [
"Coordinate",
diff --git a/sbapp/mapview/_version.py b/sbapp/mapview/_version.py
index 382021f3..8c0d5d5b 100644
--- a/sbapp/mapview/_version.py
+++ b/sbapp/mapview/_version.py
@@ -1 +1 @@
-__version__ = "1.0.6"
+__version__ = "2.0.0"
diff --git a/sbapp/mapview/clustered_marker_layer.py b/sbapp/mapview/clustered_marker_layer.py
index d51cee9d..c8098ce3 100644
--- a/sbapp/mapview/clustered_marker_layer.py
+++ b/sbapp/mapview/clustered_marker_layer.py
@@ -1,8 +1,3 @@
-# coding=utf-8
-"""
-Layer that support point clustering
-===================================
-"""
import time
import RNS
@@ -11,13 +6,7 @@ from os.path import dirname, join
from kivy.lang import Builder
from kivy.metrics import dp
-from kivy.properties import (
- ListProperty,
- NumericProperty,
- ObjectProperty,
- StringProperty,
- BooleanProperty,
-)
+from kivy.properties import ListProperty, NumericProperty, ObjectProperty, StringProperty, BooleanProperty
from mapview.view import MapLayer, MapMarker, CustomMapMarker
from kivymd.uix.button import MDIconButton
@@ -58,41 +47,26 @@ Builder.load_string(
)
-# longitude/latitude to spherical mercator in [0..1] range
-def lngX(lng):
- return lng / 360.0 + 0.5
-
+# Longitude/latitude to spherical mercator in [0..1] range
+def lngX(lng): return lng / 360.0 + 0.5
def latY(lat):
- if lat == 90:
- return 0
- if lat == -90:
- return 1
+ if lat == 90: return 0
+ if lat == -90: return 1
s = sin(lat * pi / 180.0)
y = 0.5 - 0.25 * log((1 + s) / (1 - s)) / pi
return min(1, max(0, y))
-
-# spherical mercator to longitude/latitude
-def xLng(x):
- return (x - 0.5) * 360
-
-
+# Spherical mercator to longitude/latitude
+def xLng(x): return (x - 0.5) * 360
def yLat(y):
y2 = (180 - y * 360) * pi / 180
return 360 * atan(exp(y2)) / pi - 90
-
class KDBush:
- """
- kdbush implementation from:
- https://github.com/mourner/kdbush/blob/master/src/kdbush.js
- """
-
def __init__(self, points, node_size=64):
self.points = points
self.node_size = node_size
-
self.ids = ids = [0] * len(points)
self.coords = coords = [0] * len(points) * 2
for i, point in enumerate(points):
@@ -133,28 +107,22 @@ class KDBush:
j = right
swap_item(ids, coords, left, k)
- if coords[2 * right + inc] > t:
- swap_item(ids, coords, left, right)
+ if coords[2 * right + inc] > t: swap_item(ids, coords, left, right)
while i < j:
swap_item(ids, coords, i, j)
i += 1
j -= 1
- while coords[2 * i + inc] < t:
- i += 1
- while coords[2 * j + inc] > t:
- j -= 1
+ while coords[2 * i + inc] < t: i += 1
+ while coords[2 * j + inc] > t: j -= 1
- if coords[2 * left + inc] == t:
- swap_item(ids, coords, left, j)
+ if coords[2 * left + inc] == t: swap_item(ids, coords, left, j)
else:
j += 1
swap_item(ids, coords, j, right)
- if j <= k:
- left = j + 1
- if k <= j:
- right = j - 1
+ if j <= k: left = j + 1
+ if k <= j: right = j - 1
def _swap_item(self, ids, coords, i, j):
swap = self._swap
@@ -228,8 +196,7 @@ class KDBush:
x = coords[2 * m]
y = coords[2 * m + 1]
- if sq_dist(x, y, qx, qy) <= r2:
- result.append(ids[m])
+ if sq_dist(x, y, qx, qy) <= r2: result.append(ids[m])
nextAxis = (axis + 1) % 2
@@ -249,7 +216,6 @@ class KDBush:
dy = ay - by
return dx * dx + dy * dy
-
class Cluster:
def __init__(self, x, y, num_points, id, props, extents = None):
self.x = x
@@ -262,11 +228,10 @@ class Cluster:
self.parent_id = None
self.widget = None
- # preprocess lon/lat
+ # Preprocess lon/lat
self.lon = xLng(x)
self.lat = yLat(y)
-
class Marker:
def __init__(self, lon, lat, cls=MapMarker, options=None):
self.lon = lon
@@ -274,11 +239,11 @@ class Marker:
self.cls = cls
self.options = options
- # preprocess x/y from lon/lat
+ # Preprocess x/y from lon/lat
self.x = lngX(lon)
self.y = latY(lat)
- # cluster information
+ # Cluster information
self.id = None
self.zoom = float("inf")
self.parent_id = None
@@ -393,14 +358,10 @@ class Marker:
if not self.options: self.options = {}
self.options[k] = v
- def __repr__(self):
- return f"<{self.cls} Proxy lat={self.lat} lon={self.lon}: {self.options}>"
+ def __repr__(self): return f"<{self.cls} Proxy lat={self.lat} lon={self.lon}: {self.options}>"
class SuperCluster:
- """Port of supercluster from mapbox in pure python
- """
-
def __init__(self, min_zoom=0, max_zoom=16, radius=40, extent=512, node_size=64):
self.min_zoom = min_zoom
self.max_zoom = max_zoom
@@ -408,10 +369,8 @@ class SuperCluster:
self.extent = extent
self.node_size = node_size
+ # Load an array of markers. Once loaded, the index is immutable.
def load(self, points):
- """Load an array of markers.
- Once loaded, the index is immutable.
- """
self.trees = {}
self.points = points
for index, point in enumerate(points): point.id = index
@@ -428,10 +387,9 @@ class SuperCluster:
# print("clustering", (time.time() - start) * 1000)
self.trees[self.min_zoom] = KDBush(clusters, self.node_size)
+ # For the given bbox [westLng, southLat, eastLng, northLat], and
+ # integer zoom, returns an array of clusters and markers
def get_clusters(self, bbox, zoom):
- """For the given bbox [westLng, southLat, eastLng, northLat], and
- integer zoom, returns an array of clusters and markers
- """
tree = self.trees[self._limit_zoom(zoom)]
ids = tree.range(lngX(bbox[0]), latY(bbox[3]), lngX(bbox[2]), latY(bbox[1]))
clusters = []
@@ -442,8 +400,7 @@ class SuperCluster:
return clusters
- def _limit_zoom(self, z):
- return max(self.min_zoom, min(self.max_zoom + 1, z))
+ def _limit_zoom(self, z): return max(self.min_zoom, min(self.max_zoom + 1, z))
def _cluster(self, points, zoom):
clusters = []
@@ -451,14 +408,13 @@ class SuperCluster:
trees = self.trees
r = self.radius / float(self.extent * pow(2, zoom))
- # loop through each point
for i in range(len(points)):
p = points[i]
- # if we've already visited the point at this zoom level, skip it
+ # If we've already visited the point at this zoom level, skip it
if p.zoom <= zoom: continue
p.zoom = zoom
- # find all nearby points
+ # Find all nearby points
tree = trees[zoom + 1]
neighbor_ids = tree.within(p.x, p.y, r)
@@ -522,7 +478,6 @@ class SuperCluster:
return clusters
-
class ClusterMapMarker(MapMarker):
source = StringProperty(join(dirname(__file__), "icons", "cluster_medium.png"))
cluster = ObjectProperty()
@@ -531,8 +486,8 @@ class ClusterMapMarker(MapMarker):
extents = ListProperty(None)
delegate = ObjectProperty(None)
- def on_cluster(self, instance, cluster): self.num_points = cluster.num_points
# def on_touch_down(self, touch): return False
+ def on_cluster(self, instance, cluster): self.num_points = cluster.num_points
def on_release(self):
# RNS.log(f"{self} -> {self.delegate}: {self.num_points} / {self.extents}")
e = self.extents
diff --git a/sbapp/mapview/downloader.py b/sbapp/mapview/downloader.py
index e8882273..4869aa1e 100644
--- a/sbapp/mapview/downloader.py
+++ b/sbapp/mapview/downloader.py
@@ -28,9 +28,9 @@ USER_AGENT = 'Kivy-garden.mapview'
import RNS
class Downloader:
- _instance = None
- MAX_WORKERS = 5
- CAP_TIME = 0.064 # 15 FPS
+ _instance = None
+ MAX_WORKERS = 40
+ CAP_TIME = 0.01666 # 60 FPS
@staticmethod
def instance(cache_dir=None):
@@ -42,10 +42,8 @@ class Downloader:
def __init__(self, max_workers=None, cap_time=None, **kwargs):
self.cache_dir = kwargs.get('cache_dir', CACHE_DIR)
- if max_workers is None:
- max_workers = Downloader.MAX_WORKERS
- if cap_time is None:
- cap_time = Downloader.CAP_TIME
+ if max_workers is None: max_workers = Downloader.MAX_WORKERS
+ if cap_time is None: cap_time = Downloader.CAP_TIME
self.is_paused = False
self.cap_time = cap_time
self.executor = ThreadPoolExecutor(max_workers=max_workers)
@@ -97,65 +95,47 @@ class Downloader:
while i > 0:
digit = 0
mask = 1 << (i-1)
- if (x & mask) != 0:
- digit += 1
- if (y & mask) != 0:
- digit += 2
+ if (x & mask) != 0: digit += 1
+ if (y & mask) != 0: digit += 2
quad_key.append(str(digit))
-
i -= 1
return "".join(quad_key)
def _load_tile(self, tile):
- if tile.state == "done":
- return
+ if tile.state == "done": return
cache_fn = tile.cache_fn
- if exists(cache_fn):
- # Logger.debug("Downloader: use cache {}".format(cache_fn))
- return tile.set_source, (cache_fn,)
+ if exists(cache_fn): return tile.set_source, (cache_fn,)
tile_y = tile.map_source.get_row_count(tile.zoom) - tile.tile_y - 1
- if tile.map_source.quad_key:
- uri = tile.map_source.url.format(
- q=self.__to_quad(tile.tile_x,tile_y,tile.zoom), s=choice(tile.map_source.subdomains)
- )
- else:
- uri = tile.map_source.url.format(
- z=tile.zoom, x=tile.tile_x, y=tile_y, s=choice(tile.map_source.subdomains)
- )
-
- # Logger.debug("Downloader: download(tile) {}".format(uri))
- response = requests.get(uri, headers={'User-agent': USER_AGENT}, timeout=5)
+ if tile.map_source.quad_key: uri = tile.map_source.url.format(q=self.__to_quad(tile.tile_x,tile_y,tile.zoom), s=choice(tile.map_source.subdomains))
+ else: uri = tile.map_source.url.format(z=tile.zoom, x=tile.tile_x, y=tile_y, s=choice(tile.map_source.subdomains))
+
+ response = requests.get(uri, headers={'User-agent': USER_AGENT}, timeout=10)
try:
response.raise_for_status()
data = response.content
- with open(cache_fn, "wb") as fd:
- fd.write(data)
- # Logger.debug("Downloaded {} bytes: {}".format(len(data), uri))
+ with open(cache_fn, "wb") as fd: fd.write(data)
return tile.set_source, (cache_fn,)
- except Exception as e:
- print("Downloader error: {!r}".format(e))
+ except Exception as e: RNS.log(f"Error while downloading map tile: {e}", RNS.LOG_WARNING) if RNS.sl(RNS.LOG_DEBUG) else None
def _check_executor(self, dt):
start = time()
try:
for future in as_completed(self._futures[:], 0):
self._futures.remove(future)
- try:
- result = future.result()
- except Exception:
- traceback.print_exc()
- # make an error tile?
- continue
- if result is None:
+ try: result = future.result()
+ except Exception as e:
+ RNS.log(f"Error while getting tile downloader futures result: {e}", RNS.LOG_WARNING) if RNS.sl(RNS.LOG_DEBUG) else None
+ # RNS.trace_exception(e) if RNS.sl(RNS.LOG_DEBUG) else None
continue
+
+ if result is None: continue
callback, args = result
callback(*args)
- # capped executor in time, in order to prevent too much
- # slowiness.
- # seems to works quite great with big zoom-in/out
- if time() - start > self.cap_time:
- break
- except TimeoutError:
- pass
+ # Only allow hanging around here for one
+ # frame time. This is crude, but will do
+ # for now, until a better solution is made.
+ if time() - start > self.cap_time: break
+
+ except TimeoutError: pass
diff --git a/sbapp/mapview/geojson.py b/sbapp/mapview/geojson.py
index 0c5f4c71..9b400960 100644
--- a/sbapp/mapview/geojson.py
+++ b/sbapp/mapview/geojson.py
@@ -1,4 +1,3 @@
-# coding=utf-8
"""
Geojson layer
=============
@@ -7,7 +6,6 @@ Geojson layer
Currently experimental and a work in progress, not fully optimized.
-
Supports:
- html color in properties
@@ -21,17 +19,7 @@ __all__ = ["GeoJsonMapLayer"]
import json
-from kivy.graphics import (
- Canvas,
- Color,
- Line,
- MatrixInstruction,
- Mesh,
- PopMatrix,
- PushMatrix,
- Scale,
- Translate,
-)
+from kivy.graphics import Canvas, Color, Line, MatrixInstruction, Mesh, PopMatrix, PushMatrix, Scale, Translate
from kivy.graphics.tesselator import TYPE_POLYGONS, WINDING_ODD, Tesselator
from kivy.metrics import dp
from kivy.properties import ObjectProperty, StringProperty
@@ -192,9 +180,7 @@ COLORS = {
}
-def flatten(lst):
- return [item for sublist in lst for item in sublist]
-
+def flatten(lst): return [item for sublist in lst for item in sublist]
class GeoJsonMapLayer(MapLayer):
@@ -206,18 +192,19 @@ class GeoJsonMapLayer(MapLayer):
self.first_time = True
self.initial_zoom = None
super().__init__(**kwargs)
+
with self.canvas:
self.canvas_polygon = Canvas()
self.canvas_line = Canvas()
+
with self.canvas_polygon.before:
PushMatrix()
self.g_matrix = MatrixInstruction()
self.g_scale = Scale()
self.g_translate = Translate()
- with self.canvas_polygon:
- self.g_canvas_polygon = Canvas()
- with self.canvas_polygon.after:
- PopMatrix()
+
+ with self.canvas_polygon: self.g_canvas_polygon = Canvas()
+ with self.canvas_polygon.after: PopMatrix()
def reposition(self):
vx, vy = self.parent.delta_x, self.parent.delta_y
@@ -232,6 +219,7 @@ class GeoJsonMapLayer(MapLayer):
self.g_scale.x = self.g_scale.y = diff
else:
self.g_scale.x = self.g_scale.y = 1.0
+
self.g_translate.xy = vx, vy
self.g_matrix.matrix = self.parent._scatter.transform
@@ -240,24 +228,18 @@ class GeoJsonMapLayer(MapLayer):
self.on_geojson(self, self.geojson, update=update)
self.first_time = False
+ # Traverse the whole geojson and call the func with every element found.
def traverse_feature(self, func, part=None):
- """Traverse the whole geojson and call the func with every element
- found.
- """
- if part is None:
- part = self.geojson
- if not part:
- return
+ if part is None: part = self.geojson
+ if not part: return
tp = part["type"]
if tp == "FeatureCollection":
- for feature in part["features"]:
- func(feature)
- elif tp == "Feature":
- func(part)
+ for feature in part["features"]: func(feature)
+ elif tp == "Feature": func(part)
@property
def bounds(self):
- # return the min lon, max lon, min lat, max lat
+ # Return the min lon, max lon, min lat, max lat
bounds = [float("inf"), float("-inf"), float("inf"), float("-inf")]
def _submit_coordinate(coord):
@@ -291,8 +273,7 @@ class GeoJsonMapLayer(MapLayer):
return min_lon + cx, min_lat + cy
def on_geojson(self, instance, geojson, update=False):
- if self.parent is None:
- return
+ if self.parent is None: return
if not update:
self.g_canvas_polygon.clear()
self._geojson_part(geojson, geotype="Polygon")
@@ -301,12 +282,9 @@ class GeoJsonMapLayer(MapLayer):
def on_source(self, instance, value):
if value.startswith(("http://", "https://")):
- Downloader.instance(cache_dir=self.cache_dir).download(
- value, self._load_geojson_url
- )
+ Downloader.instance(cache_dir=self.cache_dir).download(value, self._load_geojson_url)
else:
- with open(value, "rb") as fd:
- geojson = json.load(fd)
+ with open(value, "rb") as fd: geojson = json.load(fd)
self.geojson = geojson
def _load_geojson_url(self, url, response):
@@ -316,15 +294,12 @@ class GeoJsonMapLayer(MapLayer):
tp = part["type"]
if tp == "FeatureCollection":
for feature in part["features"]:
- if geotype and feature["geometry"]["type"] != geotype:
- continue
+ if geotype and feature["geometry"]["type"] != geotype: continue
self._geojson_part_f(feature)
elif tp == "Feature":
- if geotype and part["geometry"]["type"] == geotype:
- self._geojson_part_f(part)
+ if geotype and part["geometry"]["type"] == geotype: self._geojson_part_f(part)
else:
- # unhandled geojson part
- pass
+ pass # Unhandled geojson part
def _geojson_part_f(self, feature):
properties = feature["properties"]
@@ -332,10 +307,8 @@ class GeoJsonMapLayer(MapLayer):
graphics = self._geojson_part_geometry(geometry, properties)
for g in graphics:
tp = geometry["type"]
- if tp == "Polygon":
- self.g_canvas_polygon.add(g)
- else:
- self.canvas_line.add(g)
+ if tp == "Polygon": self.g_canvas_polygon.add(g)
+ else: self.canvas_line.add(g)
def _geojson_part_geometry(self, geometry, properties):
tp = geometry["type"]
@@ -354,9 +327,7 @@ class GeoJsonMapLayer(MapLayer):
color = self._get_color_from(properties.get("color", "FF000088"))
graphics.append(Color(*color))
for vertices, indices in tess.meshes:
- graphics.append(
- Mesh(vertices=vertices, indices=indices, mode="triangle_fan")
- )
+ graphics.append(Mesh(vertices=vertices, indices=indices, mode="triangle_fan"))
elif tp == "LineString":
stroke = get_color_from_hex(properties.get("stroke", "#ffffff"))
diff --git a/sbapp/mapview/mbtsource.py b/sbapp/mapview/mbtsource.py
index 2a2f69c9..319f10ae 100644
--- a/sbapp/mapview/mbtsource.py
+++ b/sbapp/mapview/mbtsource.py
@@ -1,15 +1,5 @@
-# coding=utf-8
-"""
-MBTiles provider for MapView
-============================
-
-This provider is based on .mbfiles from MapBox.
-See: http://mbtiles.org/
-"""
-
__all__ = ["MBTilesMapSource"]
-
import io
import sqlite3
import threading
@@ -20,32 +10,29 @@ from kivy.core.image import ImageLoader
from mapview.downloader import Downloader
from mapview.source import MapSource
-
class MBTilesMapSource(MapSource):
def __init__(self, filename, **kwargs):
super().__init__(**kwargs)
self.filename = filename
self.db = sqlite3.connect(filename)
- # read metadata
+ # Read metadata
c = self.db.cursor()
metadata = dict(c.execute("SELECT * FROM metadata"))
- if metadata["format"] == "pbf":
- raise ValueError("Only raster maps are supported, not vector maps.")
+ if metadata["format"] == "pbf": raise ValueError("Only raster maps are supported, not vector maps.")
self.min_zoom = int(metadata["minzoom"])
self.max_zoom = int(metadata["maxzoom"])
self.attribution = metadata.get("attribution", "")
self.bounds = bounds = None
cx = cy = 0.0
cz = 5
- if "bounds" in metadata:
- self.bounds = bounds = tuple(map(float, metadata["bounds"].split(",")))
- if "center" in metadata:
- cx, cy, cz = tuple(map(float, metadata["center"].split(",")))
+ if "bounds" in metadata: self.bounds = bounds = tuple(map(float, metadata["bounds"].split(",")))
+ if "center" in metadata: cx, cy, cz = tuple(map(float, metadata["center"].split(",")))
elif self.bounds:
cx = (bounds[2] + bounds[0]) / 2.0
cy = (bounds[3] + bounds[1]) / 2.0
cz = self.min_zoom
+
self.default_lon = cx
self.default_lat = cy
self.default_zoom = int(cz)
@@ -53,42 +40,30 @@ class MBTilesMapSource(MapSource):
self.is_xy = self.projection == "xy"
def fill_tile(self, tile):
- if tile.state == "done":
- return
+ if tile.state == "done": return
Downloader.instance(self.cache_dir).submit(self._load_tile, tile)
def _load_tile(self, tile):
# global db context cannot be shared across threads.
ctx = threading.local()
- if not hasattr(ctx, "db"):
- ctx.db = sqlite3.connect(self.filename)
+ if not hasattr(ctx, "db"): ctx.db = sqlite3.connect(self.filename)
# get the right tile
c = ctx.db.cursor()
- c.execute(
- (
- "SELECT tile_data FROM tiles WHERE "
- "zoom_level=? AND tile_column=? AND tile_row=?"
- ),
- (tile.zoom, tile.tile_x, tile.tile_y),
- )
+ c.execute( "SELECT tile_data FROM tiles WHERE zoom_level=? AND tile_column=? AND tile_row=?",
+ (tile.zoom, tile.tile_x, tile.tile_y) )
row = c.fetchone()
if not row:
tile.state = "done"
return
# no-file loading
- try:
- data = io.BytesIO(row[0])
+ try: data = io.BytesIO(row[0])
except Exception:
# android issue, "buffer" does not have the buffer interface
# ie row[0] buffer is not compatible with BytesIO on Android??
data = io.BytesIO(bytes(row[0]))
- im = CoreImage(
- data,
- ext='png',
- filename="{}.{}.{}.png".format(tile.zoom, tile.tile_x, tile.tile_y),
- )
+ im = CoreImage(data, ext='png', filename="{}.{}.{}.png".format(tile.zoom, tile.tile_x, tile.tile_y))
if im is None:
tile.state = "done"
@@ -101,21 +76,17 @@ class MBTilesMapSource(MapSource):
tile.state = "need-animation"
def get_x(self, zoom, lon):
- if self.is_xy:
- return lon
+ if self.is_xy: return lon
return super().get_x(zoom, lon)
def get_y(self, zoom, lat):
- if self.is_xy:
- return lat
+ if self.is_xy: return lat
return super().get_y(zoom, lat)
def get_lon(self, zoom, x):
- if self.is_xy:
- return x
+ if self.is_xy: return x
return super().get_lon(zoom, x)
def get_lat(self, zoom, y):
- if self.is_xy:
- return y
+ if self.is_xy: return y
return super().get_lat(zoom, y)
diff --git a/sbapp/mapview/source.py b/sbapp/mapview/source.py
index db1e1c10..2a68d02c 100644
--- a/sbapp/mapview/source.py
+++ b/sbapp/mapview/source.py
@@ -1,5 +1,3 @@
-# coding=utf-8
-
__all__ = ["MapSource"]
import hashlib
@@ -7,66 +5,35 @@ from math import atan, ceil, cos, exp, log, pi, tan
from kivy.metrics import dp
-from mapview.constants import (
- CACHE_DIR,
- MAX_LATITUDE,
- MAX_LONGITUDE,
- MIN_LATITUDE,
- MIN_LONGITUDE,
-)
+from mapview.constants import CACHE_DIR, MAX_LATITUDE, MAX_LONGITUDE, MIN_LATITUDE, MIN_LONGITUDE
from mapview.downloader import Downloader
from mapview.utils import clamp
class MapSource:
- """Base class for implementing a map source / provider
- """
-
- attribution_osm = 'Maps & Data © [i][ref=http://www.osm.org/copyright]OpenStreetMap contributors[/ref][/i]'
- attribution_ve = 'Maps © [i][ref=http://www.virtualearth.net]VirtualEarth[/ref][/i]'
+ mt_key = "TUX5omZx8Sqgh9JDquwf"
+ attribution_osm = '© OpenStreetMap contributors'
+ attribution_ve = '© VirtualEarth'
+ attribution_mt = '© MapTiler © OpenStreetMap contributors'
# list of available providers
# cache_key: (is_overlay, minzoom, maxzoom, url, attribution)
providers = {
- "osm": (
- 0,
- 0,
- 19,
- "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
- attribution_osm,
- ),
- "ve": (
- 0,
- 0,
- 19,
- "http://ecn.t3.tiles.virtualearth.net/tiles/a{q}.jpeg?g=1",
- attribution_ve,
- ),
- "osm-hot": (
- 0,
- 0,
- 19,
- "http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png",
- "",
- ),
+ "testing": (0, 0, 19, "https://api.maptiler.com/maps/topo-v4/256/{z}/{x}/{y}.png?key="+mt_key, attribution_mt, 256),
+ "osm": (0, 0, 19, "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", attribution_osm, 256),
+ "mt_outdoor": (0, 0, 19, "https://api.maptiler.com/maps/outdoor-v4/256/{z}/{x}/{y}.png?key="+mt_key, attribution_mt, 256),
+ "mt_topo": (0, 0, 19, "https://api.maptiler.com/maps/topo-v4/256/{z}/{x}/{y}.png?key="+mt_key, attribution_mt, 256),
+ "mt_hybrid": (0, 0, 19, "https://api.maptiler.com/maps/hybrid-v4/256/{z}/{x}/{y}.jpg?key="+mt_key, attribution_mt, 256),
+ "virtualearth": (0, 0, 19, "http://ecn.t3.tiles.virtualearth.net/tiles/a{q}.jpeg?g=1", attribution_ve, 256),
+ "opentopo": (0, 0, 17, "https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", attribution_osm, 256),
}
- def __init__(
- self,
- url="http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
- cache_key=None,
- min_zoom=0,
- max_zoom=19,
- tile_size=256,
- image_ext="png",
- attribution="© OpenStreetMap contributors",
- subdomains="abc",
- quad_key = False,
- **kwargs
- ):
- if cache_key is None:
- # possible cache hit, but very unlikely
- cache_key = hashlib.sha224(url.encode("utf8")).hexdigest()[:10]
+ def __init__(self, url="http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", cache_key=None,
+ min_zoom=0, max_zoom=19, tile_size=256, image_ext="png", attribution="© OpenStreetMap contributors",
+ subdomains="abc", quad_key = False, **kwargs):
+
+ # Possible cache hit, but very unlikely
+ if cache_key is None: cache_key = hashlib.sha224(url.encode("utf8")).hexdigest()[:10]
self.url = url
self.cache_key = cache_key
self.min_zoom = min_zoom
@@ -88,79 +55,53 @@ class MapSource:
provider = MapSource.providers[key]
cache_dir = kwargs.get('cache_dir', CACHE_DIR)
options = {}
- is_overlay, min_zoom, max_zoom, url, attribution = provider[:5]
- if len(provider) > 5:
- options = provider[5]
- return MapSource(
- cache_key=key,
- min_zoom=min_zoom,
- max_zoom=max_zoom,
- url=url,
- cache_dir=cache_dir,
- attribution=attribution,
- quad_key=quad_key,
- **options
- )
-
+ is_overlay, min_zoom, max_zoom, url, attribution, tile_size = provider[:6]
+ if len(provider) > 6: options = provider[6:]
+ import RNS # TODO: Remove
+ RNS.log(f"PROVIDER: {provider}")
+ RNS.log(f"OPTIONS: {options}")
+ return MapSource(cache_key=key, min_zoom=min_zoom, max_zoom=max_zoom,
+ url=url, cache_dir=cache_dir, attribution=attribution,
+ quad_key=quad_key, tile_size=tile_size, **options)
+
+ # Get the x position on the map using this map source's projection
+ # (0, 0) is located at the top left.
def get_x(self, zoom, lon):
- """Get the x position on the map using this map source's projection
- (0, 0) is located at the top left.
- """
lon = clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE)
return ((lon + 180.0) / 360.0 * pow(2.0, zoom)) * self.dp_tile_size
+ # Get the y position on the map using this map source's projection,
+ # (0, 0) is located at the top left.
def get_y(self, zoom, lat):
- """Get the y position on the map using this map source's projection
- (0, 0) is located at the top left.
- """
lat = clamp(-lat, MIN_LATITUDE, MAX_LATITUDE)
lat = lat * pi / 180.0
- return (
- (1.0 - log(tan(lat) + 1.0 / cos(lat)) / pi) / 2.0 * pow(2.0, zoom)
- ) * self.dp_tile_size
+ return ( (1.0 - log(tan(lat) + 1.0 / cos(lat)) / pi) / 2.0 * pow(2.0, zoom) ) * self.dp_tile_size
+ # Get the longitude to the x position in the map source's projection
def get_lon(self, zoom, x):
- """Get the longitude to the x position in the map source's projection
- """
dx = x / float(self.dp_tile_size)
lon = dx / pow(2.0, zoom) * 360.0 - 180.0
return clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE)
+ # Get the latitude to the y position in the map source's projection
def get_lat(self, zoom, y):
- """Get the latitude to the y position in the map source's projection
- """
dy = y / float(self.dp_tile_size)
n = pi - 2 * pi * dy / pow(2.0, zoom)
lat = -180.0 / pi * atan(0.5 * (exp(n) - exp(-n)))
return clamp(lat, MIN_LATITUDE, MAX_LATITUDE)
def get_row_count(self, zoom):
- """Get the number of tiles in a row at this zoom level
- """
- if zoom == 0:
- return 1
+ if zoom == 0: return 1
return 2 << (zoom - 1)
def get_col_count(self, zoom):
- """Get the number of tiles in a col at this zoom level
- """
- if zoom == 0:
- return 1
+ if zoom == 0: return 1
return 2 << (zoom - 1)
- def get_min_zoom(self):
- """Return the minimum zoom of this source
- """
- return self.min_zoom
-
- def get_max_zoom(self):
- """Return the maximum zoom of this source
- """
- return self.max_zoom
+ def get_min_zoom(self): return self.min_zoom
+ def get_max_zoom(self): return self.max_zoom
+ # Add this tile to load within the downloader
def fill_tile(self, tile):
- """Add this tile to load within the downloader
- """
- if tile.state == "done":
- return
+ if tile.state == "done": return
Downloader.instance(cache_dir=self.cache_dir).download_tile(tile)
diff --git a/sbapp/mapview/types.py b/sbapp/mapview/types.py
index 622d8a90..125c8d40 100644
--- a/sbapp/mapview/types.py
+++ b/sbapp/mapview/types.py
@@ -1,29 +1,22 @@
-# coding=utf-8
-
__all__ = ["Coordinate", "Bbox"]
from collections import namedtuple
Coordinate = namedtuple("Coordinate", ["lat", "lon"])
-
class Bbox(tuple):
def collide(self, *args):
if isinstance(args[0], Coordinate):
coord = args[0]
lat = coord.lat
lon = coord.lon
- else:
- lat, lon = args
+ else: lat, lon = args
+
lat1, lon1, lat2, lon2 = self[:]
- if lat1 < lat2:
- in_lat = lat1 <= lat <= lat2
- else:
- in_lat = lat2 <= lat <= lat2
- if lon1 < lon2:
- in_lon = lon1 <= lon <= lon2
- else:
- in_lon = lon2 <= lon <= lon2
+ if lat1 < lat2: in_lat = lat1 <= lat <= lat2
+ else: in_lat = lat2 <= lat <= lat2
+ if lon1 < lon2: in_lon = lon1 <= lon <= lon2
+ else: in_lon = lon2 <= lon <= lon2
return in_lat and in_lon
diff --git a/sbapp/mapview/utils.py b/sbapp/mapview/utils.py
index 1ecc84f4..d65b6882 100644
--- a/sbapp/mapview/utils.py
+++ b/sbapp/mapview/utils.py
@@ -1,5 +1,3 @@
-# coding=utf-8
-
__all__ = ["clamp", "haversine", "get_zoom_for_radius"]
from math import asin, cos, pi, radians, sin, sqrt
diff --git a/sbapp/mapview/view.py b/sbapp/mapview/view.py
index 2abaf9ae..a59323e2 100644
--- a/sbapp/mapview/view.py
+++ b/sbapp/mapview/view.py
@@ -1,5 +1,3 @@
-# coding=utf-8
-
__all__ = ["MapView", "MapMarker", "MapMarkerPopup", "MapLayer", "MarkerMapLayer"]
import webbrowser
diff --git a/sbapp/sideband/core.py b/sbapp/sideband/core.py
index e3d10522..de79b9a1 100644
--- a/sbapp/sideband/core.py
+++ b/sbapp/sideband/core.py
@@ -979,7 +979,7 @@ class SidebandCore():
self.config["last_lxmf_propagation_node"] = dest
self.message_router.set_outbound_propagation_node(dest)
- RNS.log("Active propagation node set to: "+RNS.prettyhexrep(dest), RNS.LOG_VERBOSE)
+ RNS.log("Active propagation node set to: "+RNS.prettyhexrep(dest), RNS.LOG_DEBUG)
self.__save_config()
except Exception as e: RNS.log("Error while setting LXMF propagation node: "+str(e), RNS.LOG_ERROR)
@@ -1016,7 +1016,7 @@ class SidebandCore():
try:
if app_data == None: app_data = b""
if type(app_data) != bytes: app_data = msgpack.packb([app_data, stamp_cost])
- RNS.log("Received "+str(dest_type)+" announce for "+RNS.prettyhexrep(dest), RNS.LOG_DEBUG)
+ RNS.log("Received "+str(dest_type)+" announce for "+RNS.prettyhexrep(dest), RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
self._db_save_announce(dest, app_data, dest_type, link_stats)
self.setstate("app.flags.new_announces", True)
diff --git a/sbapp/ui/map.py b/sbapp/ui/map.py
index deb682d1..0ca2f173 100644
--- a/sbapp/ui/map.py
+++ b/sbapp/ui/map.py
@@ -207,7 +207,7 @@ class Map():
def get_source(self):
source = None
if self.app.sideband.config["map_use_offline"]: source = self.get_offline_source()
- if source == None: source = MapSource.from_provider("osm", cache_dir=self.map_cache, quad_key=False)
+ if source == None: source = MapSource.from_provider("testing", cache_dir=self.map_cache, quad_key=False)
return source
def update_source(self, source=None):
@@ -232,7 +232,6 @@ class Map():
if nlon > 179: nlon = 179
self.map.center_on(nlat,nlon)
-
self.map.map_source = ns
def interfaces_action(self, sender=None):
@@ -251,8 +250,13 @@ class Map():
layers = []
if self.app.sideband.config["map_use_offline"]: layers.append("offline")
if self.app.sideband.config["map_use_online"]:
+ layers.append("testing")
layers.append("osm")
- layers.append("ve")
+ layers.append("mt_outdoor")
+ layers.append("mt_topo")
+ layers.append("mt_hybrid")
+ layers.append("opentopo")
+ layers.append("virtualearth")
if ml == None: ml = layers[0]
if not ml in layers: ml = layers[0]
@@ -262,15 +266,24 @@ class Map():
ml = layers[mli]
source = None
- if ml == "offline": source = self.get_offline_source()
- if ml == "osm": source = MapSource.from_provider("osm", cache_dir=self.map_cache, quad_key=False)
- if ml == "ve": source = MapSource.from_provider("ve", cache_dir=self.map_cache, quad_key=True)
+ if ml == "offline": source = self.get_offline_source()
+ elif ml == "testing": source = MapSource.from_provider("testing", cache_dir=self.map_cache, quad_key=False)
+ elif ml == "osm": source = MapSource.from_provider("osm", cache_dir=self.map_cache, quad_key=False)
+ elif ml == "mt_outdoor": source = MapSource.from_provider("mt_outdoor", cache_dir=self.map_cache, quad_key=False)
+ elif ml == "mt_topo": source = MapSource.from_provider("mt_topo", cache_dir=self.map_cache, quad_key=False)
+ elif ml == "mt_hybrid": source = MapSource.from_provider("mt_hybrid", cache_dir=self.map_cache, quad_key=False)
+ elif ml == "opentopo": source = MapSource.from_provider("opentopo", cache_dir=self.map_cache, quad_key=False)
+ elif ml == "virtualearth": source = MapSource.from_provider("virtualearth", cache_dir=self.map_cache, quad_key=True)
+
+ toast(f"Using map \"{ml}\"")
if source != None:
self.map_layer = ml
self.update_source(source)
- except Exception as e: RNS.log("Error while switching map layer: "+str(e), RNS.LOG_ERROR)
+ except Exception as e:
+ RNS.log("Error while switching map layer: "+str(e), RNS.LOG_ERROR)
+ RNS.trace_exception(e)
map_nav_divisor = 12
map_nav_zoom = 0.25
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────